﻿#region Using Directives

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Kongo.Properties;

#endregion Using Directives


namespace Kongo
{
    /// <summary>
    /// Encapsulates information used to render a <see cref="Template"/>.
    /// </summary>
    public class Context
    {
        #region Fields

        private static readonly Regex regex = new Regex("^[A-Za-z_][A-Za-z_0-9]*$");
        private string invalidVariableOutput;
        private Stack<Dictionary<string, object>> stack;

        #endregion Fields


        #region Methods

        internal void Dump(TextWriter output)
        {
            // Dump the context to the output stream.
            // Increase the indent for inner scopes.
            int indent = 0;
            Dictionary<string, object>[] dicts = stack.ToArray();
            for (int i = dicts.Length - 1; i >= 0; i--)
            {
                foreach (KeyValuePair<string, object> kvp in dicts[i])
                {
                    output.Write(new string('\t', indent));
                    output.Write(kvp.Key);
                    output.Write("=");
                    output.WriteLine(kvp.Value);
                }
                indent++;
            }
        }


        /// <summary>
        /// Explicitly removes the innermost scope of variables in the context.
        /// </summary>
        /// <exception cref="InvalidOperationException">The scope is empty.</exception>
        public void Pop()
        {
            // Never let it get below one dictionary in the stack
            if (stack.Count == 1)
                throw new InvalidOperationException(Resource.InvalidOperation_EmptyScope);

            stack.Pop();
        }


        /// <summary>
        /// Explicitly creates a new scope of variables in the context.
        /// </summary>
        public void Push()
        {
            // Push a new dictionary onto the stack
            stack.Push(new Dictionary<string, object>());
        }


        /// <summary>
        /// Gets the value associated with the specified key by searching from the innermost to the outermost scope.
        /// </summary>
        /// <param name="key">The key of the value to get.</param>
        /// <param name="value">
        /// When this method returns, contains the value associated with the specified key, if
        /// the key is found; otherwise, null. This parameter is passed uninitialized.</param>
        /// <returns>true if the <see cref="Context" /> contains an element with the specified key; otherwise, false.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key" /> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="key"/> is an empty <see cref="String"/>.</exception>
        public bool TryGetValue(string key, out object value)
        {
            Helpers.ThrowIfNullOrEmpty(key, "key");

            // Walk up the stack looking for the variable
            foreach (Dictionary<string, object> dict in stack)
            {
                if (dict.TryGetValue(key, out value))
                    return true;
            }

            value = null;
            return false;
        }

        #endregion Methods


        #region Properties

        /// <summary>
        /// Gets or sets the output used when a <see cref="Variable"/> cannot be found.
        /// </summary>
        /// <returns>The <see cref="String"/> to substitute for invalid variables.</returns>
        public string InvalidVariableOutput
        {
            get { return invalidVariableOutput; }
            set
            {
                Helpers.ThrowIfNull(value, "InvalidVariableOutput");
                this.invalidVariableOutput = value;
            }
        }

        #endregion Properties


        #region Indexers

        /// <summary>
        /// Gets or sets the data stored in the variable specified by name.
        /// </summary>
        /// <param name="key">The name of the variable.</param>
        /// <returns>An <see cref="Object"/> that contains the data.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key" /> is null.</exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="key"/> is an empty <see cref="String"/>.
        /// -or-
        /// <paramref name="key"/> is not a valid <see cref="Variable"/> name.
        /// </exception>
        /// <exception cref="KeyNotFoundException">The <paramref name="key" /> does not exist in the collection.</exception>
        public object this[string key]
        {
            get
            {
                object val;
                if (TryGetValue(key, out val))
                    return val;

                throw new KeyNotFoundException();
            }
            set
            {
                Helpers.ThrowIfNullOrEmpty(key, "key");
                if (!regex.IsMatch(key))
                    throw new ArgumentException(Helpers.Format(Resource.Argument_InvalidExpression, "character"));

                stack.Peek()[key] = value;
            }
        }

        #endregion Indexers


        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Context"/> class.
        /// </summary>
        public Context()
        {
            this.invalidVariableOutput = String.Empty;

            // The stack must always have at least one dictionary
            this.stack = new Stack<Dictionary<string, object>>();
            this.stack.Push(new Dictionary<string, object>());
        }

        #endregion Constructors
    }
}
